package guacd

import (
	"fmt"
	"gitee.com/ling1314/origin-utility/constant"
	"gitee.com/ling1314/origin-utility/model"
	"gitee.com/ling1314/origin-utility/modules/repository"
	"gitee.com/ling1314/origin-utility/utils"
	"github.com/gin-gonic/gin"
	"github.com/gorilla/websocket"
	log "github.com/sirupsen/logrus"
	"net/http"
	"strconv"
	"time"
)

const (
	TunnelClosed             int = -1
	Normal                   int = 0
	NotFoundSession          int = 800
	NewTunnelError           int = 801
	ForcedBreak              int = 802
	AccessGatewayUnAvailable int = 803
	AccessGatewayCreateError int = 804
	AssetNotActive           int = 805
	ForcedDestroy            int = 806
)

const (
	GUACDADDR = "192.168.51.39"
	GUACDPORT = 4822
)

var UpGrader = websocket.Upgrader{
	CheckOrigin: func(r *http.Request) bool {
		return true
	},
	// 在原始websocket中添加对guacamole协议的支持
	Subprotocols: []string{"guacamole"},
}

type GuacamoleApi struct {
}

func (api GuacamoleApi) Guacamole(c *gin.Context) error {
	ws, err := UpGrader.Upgrade(c.Writer, c.Request, nil)
	if err != nil {
		log.Errorf("升级为WebSocket协议失败：%v \n", err.Error())
		return err
	}
	var input model.OpenAppDesktopInput
	if err = c.ShouldBindQuery(&input); err != nil {
		log.Error(err.Error())
		return err
	}
	var (
		isExist       bool
		guacdTunnel   *Tunnel
		s             model.HostSession
		configuration *Configuration
		propertyMap   map[string]string
	)
	// 查询数据库中的会话记录，因为前端在发送websocket前已经发送了一次ajax通知服务端写入会话记录了,现在发起ws时需要更新其中的状态
	s, err = repository.HostSessionRepository.FindById(input.HostSessionId)
	if err != nil {
		return err
	}
	configuration = NewConfiguration()
	propertyMap = repository.PropertyRepository.FindAllMap()
	configuration.SetParameter("width", input.Width)
	configuration.SetParameter("height", input.Height)
	configuration.SetParameter("dpi", input.Dpi)
	configuration.SetParameter("static-channels", constant.LauncherVisualChannelName)
	configuration.SetParameter("remote-app", constant.LauncherPath)
	// todo 根据全局配置、gateway等等配置全局参数。后面在补,这里只写入了基本参数
	api.setConfig(propertyMap, s, configuration)
	configuration.SetParameter("hostname", s.HostIp)
	configuration.SetParameter("port", strconv.Itoa(s.ConnectPort))
	// todo 动态改造
	addr := GUACDADDR + ":" + strconv.Itoa(GUACDPORT)
	log.Debugf("[%v] 新建 guacd 会话, guacd=%v, asset=%v", input.HostSessionId, addr,
		fmt.Sprintf("%s:%s", configuration.GetParameter("hostname"), configuration.GetParameter("port")))
	log.Debugf("连接参数=%v", configuration.Parameters)
	if input.IsReuse {
		configuration.ConnectionID = s.ConnectionId
	}
	// 否则就是要新开tunnel
	guacdTunnel, err = NewTunnel(addr, configuration)
	if err != nil {
		log.Printf("[%v] 建立连接失败: %v", input.HostSessionId, err.Error())
		Disconnect(ws, NewTunnelError, err.Error())
		return err
	}
	// 更新数据库中的会话状态
	s.Width = input.Width
	s.Height = input.Height
	isExist = GlobalSessionManager.IsExist(input.HostSessionId)
	if input.IsReuse && isExist {
		log.Debugln("复用会话")
	} else {
		s.ConnectionId = guacdTunnel.UUID
		s.HoldStatus = model.VaSessionHoldForHolding
		s.Status = model.VaSessionStatusForInit
		s.ConnectedTime = time.Now().Unix()
		log.Debugln("新建会话，将会话纳入管理")
		// 将相关连接参数封装到 session对象中
		nextSession := &Session{
			ID:          input.HostSessionId,
			Protocol:    s.Protocol,
			Mode:        constant.Guacd,
			WebSocket:   ws,
			GuacdTunnel: guacdTunnel,
		}
		// 将 session对象保存到全局对象中
		nextSession.Observer = NewObserver(input.HostSessionId)
		GlobalSessionManager.Add <- nextSession
		// 同时开启一个携程，检测这个会话的状态变化
		go nextSession.Observer.Start()

	}
	if err = repository.HostSessionRepository.UpdateById(&s); err != nil {
		return err
	}
	// 创建guacamoleHandler ，它提供了两个协程，用来读写 ws 和 tunnel 中的数据
	guacamoleHandler := NewGuacamoleHandler(ws, guacdTunnel)
	guacamoleHandler.Start()
	// 开启循环，读取websocket中的信息并发送到tunnel中，消息不是直接发送，而是通过 channel
	// java中通过lock来实现有序发送，go中的channel消息默认就是有系的。
	// 如果ws或者tunnel中读取消息时发生错误，则其中一方已经断开，执行关闭会话的操作,即：关闭连接，修改数据库中的会话状态。
	for {
		var message []byte
		_, message, err = ws.ReadMessage()
		if err != nil {
			guacamoleHandler.Stop()
			return nil
		}
		if string(message) == "10.disconnect;" {
			// 不接受前端发送的销毁信号，rdp断开、相关的会话销毁只能由go监听到会话销毁后移除
			// 必须要周期性发送 ping信号，否则guacd会判定读超时，连接会在1s左右被移除
			go func() {
				for _ = range time.Tick(time.Second) {
					instruction := NewInstruction("ping", fmt.Sprintf("%d", time.Now().Unix()))
					_, err = guacdTunnel.WriteAndFlush([]byte(instruction.String()))
					if err != nil {
						log.Error(err)
						return
					}

				}

			}()
		} else {
			_, err = guacdTunnel.WriteAndFlush(message)
			if err != nil {
				// todo 操作数据库
				//service.SessionService.CloseSessionById(sessionId, TunnelClosed, "远程连接已关闭")
				return nil
			}
		}
	}
}

func (api GuacamoleApi) GuacamoleMonitor(c *gin.Context) error {
	ws, err := UpGrader.Upgrade(c.Writer, c.Request, nil)
	if err != nil {
		log.Errorf("升级为WebSocket协议失败：%v", err.Error())
		return err
	}
	sid := c.Query("id")
	var sessionId int64
	sessionId, err = strconv.ParseInt(sid, 10, 64)
	if err != nil {
		log.Error("转换会话ID时发生错误", err)
		return err
	}
	var s model.HostSession
	if s, err = repository.HostSessionRepository.FindById(sessionId); err != nil {
		return err
	}
	if s.HoldStatus != model.VaSessionHoldForHolding {
		Disconnect(ws, AssetNotActive, "会话离线")
		return nil
	}
	connectionId := s.ConnectionId
	configuration := NewConfiguration()
	configuration.ConnectionID = connectionId
	sessionId = s.HostSessionId
	configuration.SetParameter("width", s.Width)
	configuration.SetParameter("height", s.Width)
	configuration.SetParameter("dpi", "96")

	addr := GUACDADDR + ":" + strconv.Itoa(GUACDPORT)
	asset := fmt.Sprintf("%s:%s", configuration.GetParameter("hostname"), configuration.GetParameter("port"))
	log.Debugf("[%v] 新建 guacd 会话, guacd=%v, asset=%v", sessionId, addr, asset)

	guacdTunnel, err := NewTunnel(addr, configuration)
	if err != nil {
		Disconnect(ws, NewTunnelError, err.Error())
		log.Printf("[%v] 建立连接失败: %v", sessionId, err.Error())
		return err
	}

	nextSession := &Session{
		ID:          sessionId,
		Protocol:    s.Protocol,
		Mode:        constant.Guacd,
		WebSocket:   ws,
		GuacdTunnel: guacdTunnel,
	}

	// 要监控会话
	forObsSession := GlobalSessionManager.GetById(sessionId)
	if forObsSession == nil {
		Disconnect(ws, NotFoundSession, "获取会话失败")
		return nil
	}
	nextSession.ID = utils.GetSoftId()
	forObsSession.Observer.Add <- nextSession
	log.Debugf("[%v:%v] 观察者[%v]加入会话[%v]", sessionId, connectionId, nextSession.ID, s.ConnectionId)

	guacamoleHandler := NewGuacamoleHandler(ws, guacdTunnel)
	guacamoleHandler.Start()
	for {
		_, message, err := ws.ReadMessage()
		if err != nil {
			log.Debugf("[%v:%v] WebSocket已关闭, %v", sessionId, connectionId, err.Error())
			// guacdTunnel.Read() 会阻塞，所以要先把guacdTunnel客户端关闭，才能退出Guacd循环
			_ = guacdTunnel.Close()

			observerId := nextSession.ID
			forObsSession.Observer.Del <- observerId
			log.Debugf("[%v:%v] 观察者[%v]退出会话", sessionId, connectionId, observerId)
			guacamoleHandler.Stop()
			return nil
		}
		_, err = guacdTunnel.WriteAndFlush(message)
		if err != nil {
			// todo 操作数据库
			// service.SessionService.CloseSessionById(sessionId, TunnelClosed, "远程连接已关闭")
			return nil
		}
	}
}

func (api GuacamoleApi) setConfig(propertyMap map[string]string, s model.HostSession, configuration *Configuration) {
	//if propertyMap[EnableRecording] == "true" {
	//	// todo recording 后期需要修改为动态的
	//	configuration.SetParameter(RecordingPath, path.Join("data/recording", strconv.FormatInt(s.SessionId, 10)))
	//	configuration.SetParameter(CreateRecordingPath, "true")
	//} else {
	//	configuration.SetParameter(RecordingPath, "")
	//}

	configuration.Protocol = s.Protocol
	switch configuration.Protocol {
	case "rdp":
		configuration.SetParameter("username", s.WinAccount)
		configuration.SetParameter("password", s.WinPwd)

		//configuration.SetParameter("security", "any")
		configuration.SetParameter("ignore-cert", "true")
		//configuration.SetParameter("create-drive-path", "true")
		//configuration.SetParameter("resize-method", "reconnect")
		//configuration.SetParameter(EnableWallpaper, propertyMap[EnableWallpaper])
		//configuration.SetParameter(EnableTheming, propertyMap[EnableTheming])
		//configuration.SetParameter(EnableFontSmoothing, propertyMap[EnableFontSmoothing])
		//configuration.SetParameter(EnableFullWindowDrag, propertyMap[EnableFullWindowDrag])
		//configuration.SetParameter(EnableDesktopComposition, propertyMap[EnableDesktopComposition])
		//configuration.SetParameter(EnableMenuAnimations, propertyMap[EnableMenuAnimations])
		//configuration.SetParameter(DisableBitmapCaching, propertyMap[DisableBitmapCaching])
		//configuration.SetParameter(DisableOffscreenCaching, propertyMap[DisableOffscreenCaching])
		//configuration.SetParameter(ColorDepth, propertyMap[ColorDepth])
		//configuration.SetParameter(ForceLossless, propertyMap[ForceLossless])
	case "ssh":
		configuration.SetParameter(FontSize, propertyMap[FontSize])
		configuration.SetParameter(FontName, propertyMap[FontName])
		configuration.SetParameter(ColorScheme, propertyMap[ColorScheme])
		configuration.SetParameter(Backspace, propertyMap[Backspace])
		configuration.SetParameter(TerminalType, propertyMap[TerminalType])
		configuration.SetParameter("username", s.WinAccount)
		configuration.SetParameter("password", s.WinPwd)
	case "vnc":
		configuration.SetParameter("username", s.WinAccount)
		configuration.SetParameter("password", s.WinPwd)
	case "telnet":
		configuration.SetParameter("username", s.WinAccount)
		configuration.SetParameter("password", s.WinPwd)

		configuration.SetParameter(FontSize, propertyMap[FontSize])
		configuration.SetParameter(FontName, propertyMap[FontName])
		configuration.SetParameter(ColorScheme, propertyMap[ColorScheme])
		configuration.SetParameter(Backspace, propertyMap[Backspace])
		configuration.SetParameter(TerminalType, propertyMap[TerminalType])
	case "kubernetes":
		configuration.SetParameter(FontSize, propertyMap[FontSize])
		configuration.SetParameter(FontName, propertyMap[FontName])
		configuration.SetParameter(ColorScheme, propertyMap[ColorScheme])
		configuration.SetParameter(Backspace, propertyMap[Backspace])
		configuration.SetParameter(TerminalType, propertyMap[TerminalType])
	default:

	}
}
